/**
* Copyright (c) 2005-2012 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.js.interactive_console.console;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.XmlRpcHandler;
import org.apache.xmlrpc.XmlRpcRequest;
import org.apache.xmlrpc.server.XmlRpcHandlerMapping;
import org.apache.xmlrpc.server.XmlRpcNoSuchHandlerException;
import org.apache.xmlrpc.server.XmlRpcServer;
import org.apache.xmlrpc.webserver.WebServer;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.python.pydev.core.FullRepIterable;
import org.python.pydev.core.ICompletionState;
import org.python.pydev.core.IToken;
import org.python.pydev.core.log.Log;
import org.python.pydev.editor.codecompletion.AbstractPyCodeCompletion;
import org.python.pydev.editor.codecompletion.IPyCompletionProposal;
import org.python.pydev.editor.codecompletion.PyCalltipsContextInformation;
import org.python.pydev.editor.codecompletion.PyCodeCompletionImages;
import org.python.pydev.editor.codecompletion.PyCompletionProposal;
import org.python.pydev.editor.codecompletion.PyLinkedModeCompletionProposal;
import com.aptana.interactive_console.console.IScriptConsoleCommunication;
import com.aptana.interactive_console.console.IXmlRpcClient;
import com.aptana.interactive_console.console.InterpreterResponse;
import com.aptana.interactive_console.console.ScriptXmlRpcClient;
import com.aptana.js.interactive_console.console.env.UserCanceledException;
import com.aptana.js.interactive_console.console.prefs.InteractiveConsolePrefs;
import com.aptana.shared_core.callbacks.ICallback;
import com.aptana.shared_core.io.ThreadStreamReader;
import com.aptana.shared_core.structure.Tuple;
/**
* Communication with Xml-rpc with the client.
*
* @author Fabio
*/
public class JSConsoleCommunication implements IScriptConsoleCommunication, XmlRpcHandler {
/**
* XML-RPC client for sending messages to the server.
*/
private IXmlRpcClient client;
/**
* Responsible for getting the stdout of the process.
*/
private final ThreadStreamReader stdOutReader;
/**
* Responsible for getting the stderr of the process.
*/
private final ThreadStreamReader stdErrReader;
/**
* This is the server responsible for giving input to a raw_input() requested.
*/
private WebServer webServer;
/**
* Initializes the xml-rpc communication.
*
* @param port the port where the communication should happen.
* @param process this is the process that was spawned (server for the XML-RPC)
*
* @throws MalformedURLException
*/
public JSConsoleCommunication(int port, Process process, int clientPort) throws Exception {
stdOutReader = new ThreadStreamReader(process.getInputStream());
stdErrReader = new ThreadStreamReader(process.getErrorStream());
stdOutReader.start();
stdErrReader.start();
//start the server that'll handle input requests
this.webServer = new WebServer(clientPort);
XmlRpcServer serverToHandleRawInput = this.webServer.getXmlRpcServer();
serverToHandleRawInput.setHandlerMapping(new XmlRpcHandlerMapping() {
public XmlRpcHandler getHandler(String handlerName) throws XmlRpcNoSuchHandlerException, XmlRpcException {
return JSConsoleCommunication.this;
}
});
this.webServer.start();
IXmlRpcClient client = new ScriptXmlRpcClient(process, stdErrReader, stdOutReader);
client.setPort(port);
this.client = client;
}
/**
* Stops the communication with the client (passes message for it to quit).
*/
public void close() throws Exception {
if (this.client != null) {
Job job = new Job("Close console communication") {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
JSConsoleCommunication.this.client.execute("close", new Object[0]);
} catch (Exception e) {
//Ok, we can ignore this one on close.
}
JSConsoleCommunication.this.client = null;
return Status.OK_STATUS;
}
};
job.schedule(); //finish it
}
if (this.webServer != null) {
this.webServer.shutdown();
this.webServer = null;
}
}
/**
* Variables that control when we're expecting to give some input to the server or when we're
* adding some line to be executed
*/
/**
* Signals that the next command added should be sent as an input to the server.
*/
private volatile boolean waitingForInput;
/**
* Input that should be sent to the server (waiting for raw_input)
*/
private volatile String inputReceived;
/**
* Response that should be sent back to the shell.
*/
private volatile InterpreterResponse nextResponse;
/**
* Helper to keep on busy loop.
*/
private volatile Object lock = new Object();
/**
* Helper to keep on busy loop.
*/
private volatile Object lock2 = new Object();
/**
* Keeps a flag indicating that we were able to communicate successfully with the shell at least once
* (if we haven't we may retry more than once the first time, as jython can take a while to initialize
* the communication)
*/
private volatile boolean firstCommWorked = false;
/**
* Called when the server is requesting some input from this class.
*/
public Object execute(XmlRpcRequest request) throws XmlRpcException {
waitingForInput = true;
inputReceived = null;
boolean needInput = true;
String stdOutContents = stdOutReader.getAndClearContents();
String stderrContents = stdErrReader.getAndClearContents();
//let the busy loop from execInterpreter free and enter a busy loop
//in this function until execInterpreter gives us an input
setNextResponse(new InterpreterResponse(stdOutContents, stderrContents, false, needInput));
//busy loop until we have an input
while (inputReceived == null) {
synchronized (lock) {
try {
lock.wait(10);
} catch (InterruptedException e) {
Log.log(e);
}
}
}
return inputReceived;
}
/**
* Executes a given line in the interpreter.
*
* @param command the command to be executed in the client
*/
public void execInterpreter(final String command, final ICallback<Object, InterpreterResponse> onResponseReceived,
final ICallback<Object, Tuple<String, String>> onContentsReceived) {
setNextResponse(null);
if (waitingForInput) {
inputReceived = command;
waitingForInput = false;
//the thread that we started in the last exec is still alive if we were waiting for an input.
} else {
//create a thread that'll keep locked until an answer is received from the server.
Job job = new Job("JS Console Communication") {
/**
* Executes the needed command
*
* @return a tuple with (null, more) or (error, false)
*
* @throws XmlRpcException
*/
private Tuple<String, Boolean> exec() throws XmlRpcException {
if (client == null) {
return new Tuple<String, Boolean>(
"JSConsoleCommunication.client is null (cannot communicate with server).", false);
}
Object execute = client.execute("addExec", new Object[] { command });
Object object = execute;
if (execute instanceof Object[]) {
Object[] objects = (Object[]) execute;
object = objects[0];
}
boolean more;
String errorContents = null;
if (object instanceof Boolean) {
more = (Boolean) object;
} else {
String str = object.toString();
String lower = str.toLowerCase();
if (lower.equals("true") || lower.equals("1")) {
more = true;
} else if (lower.equals("false") || lower.equals("0")) {
more = false;
} else {
more = false;
errorContents = str;
}
}
return new Tuple<String, Boolean>(errorContents, more);
}
@Override
protected IStatus run(IProgressMonitor monitor) {
boolean needInput = false;
try {
Tuple<String, Boolean> executed = null;
//the 1st time we'll do a connection attempt, we can try to connect n times (until the 1st time the connection
//is accepted) -- that's mostly because the server may take a while to get started.
int commAttempts = 0;
int maximumAttempts = InteractiveConsolePrefs.getMaximumAttempts();
//System.out.println(maximumAttempts);
while (true) {
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
executed = exec();
//executed.o1 is not null only if we had an error
String refusedConnPattern = "Failed to read servers response"; // Was "refused", but it didn't
// work on non English system
// (in Spanish localized systems
// it is "rechazada")
// This string always works,
// because it is hard-coded in
// the XML-RPC library)
if (executed.o1 != null && executed.o1.indexOf(refusedConnPattern) != -1) {
if (firstCommWorked) {
break;
} else {
if (commAttempts < maximumAttempts) {
commAttempts += 1;
Thread.sleep(250);
executed.o1 = stdErrReader.getAndClearContents();
continue;
} else {
break;
}
}
} else {
break;
}
//unreachable code!! -- commented because eclipse will complain about it
//throw new RuntimeException("Can never get here!");
}
firstCommWorked = true;
String errorContents = executed.o1;
boolean more = executed.o2;
String stdOutContents;
if (errorContents == null) {
errorContents = stdErrReader.getAndClearContents();
} else {
errorContents += "\n" + stdErrReader.getAndClearContents();
}
stdOutContents = stdOutReader.getAndClearContents();
setNextResponse(new InterpreterResponse(stdOutContents, errorContents, more, needInput));
} catch (Exception e) {
Log.log(e);
setNextResponse(new InterpreterResponse("", "Exception while pushing line to console:"
+ e.getMessage(), false, needInput));
}
return Status.OK_STATUS;
}
};
job.schedule();
}
int i = 500; //only get contents each 500 millis...
//busy loop until we have a response
while (nextResponse == null) {
synchronized (lock2) {
try {
lock2.wait(20);
} catch (InterruptedException e) {
// Log.log(e);
}
}
i -= 20;
if (i <= 0 && nextResponse == null) {
i = 250; //after the first, get it each 250 millis
String stderrContents = stdErrReader.getAndClearContents();
String stdOutContents = stdOutReader.getAndClearContents();
if (stdOutContents.length() > 0 || stderrContents.length() > 0) {
onContentsReceived.call(new Tuple<String, String>(stdOutContents, stderrContents));
}
}
}
onResponseReceived.call(nextResponse);
}
/**
* @return completions from the client
*/
public ICompletionProposal[] getCompletions(String text, String actTok, int offset) throws Exception {
if (waitingForInput) {
return new ICompletionProposal[0];
}
Object fromServer = client.execute("getCompletions", new Object[] { text, actTok });
List<ICompletionProposal> ret = new ArrayList<ICompletionProposal>();
convertToICompletions(text, actTok, offset, fromServer, ret);
ICompletionProposal[] proposals = ret.toArray(new ICompletionProposal[ret.size()]);
return proposals;
}
public static void convertToICompletions(String text, String actTok, int offset, Object fromServer,
List<ICompletionProposal> ret) {
if (fromServer instanceof Object[]) {
Object[] objects = (Object[]) fromServer;
fromServer = Arrays.asList(objects);
}
if (fromServer instanceof List) {
int length = actTok.lastIndexOf('.');
if (length == -1) {
length = actTok.length();
} else {
length = actTok.length() - length - 1;
}
List comps = (List) fromServer;
for (Object o : comps) {
if (o instanceof Object[]) {
//name, doc, args, type
Object[] comp = (Object[]) o;
String name = (String) comp[0];
String docStr = (String) comp[1];
int type = extractInt(comp[3]);
String args = AbstractPyCodeCompletion.getArgs((String) comp[2], type,
ICompletionState.LOOKING_FOR_INSTANCED_VARIABLE);
String nameAndArgs = name + args;
int priority = IPyCompletionProposal.PRIORITY_DEFAULT;
if (type == IToken.TYPE_LOCAL) {
priority = IPyCompletionProposal.PRIORITY_LOCALS;
} else if (type == IToken.TYPE_PARAM) {
priority = IPyCompletionProposal.PRIORITY_LOCALS_1;
}
// ret.add(new PyCompletionProposal(name,
// offset-length, length, name.length(),
// PyCodeCompletionImages.getImageForType(type), name, null, docStr, priority));
int cursorPos = name.length();
if (args.length() > 1) {
cursorPos += 1;
}
int replacementOffset = offset - length;
PyCalltipsContextInformation pyContextInformation = null;
if (args.length() > 2) {
pyContextInformation = new PyCalltipsContextInformation(args, replacementOffset + name.length()
+ 1); //just after the parenthesis
} else {
//Support for IPython completions (non standard names)
//i.e.: %completions, cd ...
if (name.length() > 0) {
//magic ipython stuff (starting with %)
if (name.charAt(0) == '%') {
replacementOffset -= 1;
} else if (name.charAt(0) == '/') {
//Should be something as cd c:/temp/foo (and name is /temp/foo)
char[] chars = text.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (c == name.charAt(0)) {
String sub = text.substring(i, text.length());
if (name.startsWith(sub)) {
replacementOffset -= (sub.length() - FullRepIterable.getLastPart(actTok)
.length());
break;
}
}
}
}
}
}
ret.add(new PyLinkedModeCompletionProposal(nameAndArgs, replacementOffset, length, cursorPos,
PyCodeCompletionImages.getImageForType(type), nameAndArgs, pyContextInformation, docStr,
priority, PyCompletionProposal.ON_APPLY_DEFAULT, args, false));
}
}
}
}
/**
* Extracts an int from an object
*
* @param objToGetInt the object that should be gotten as an int
* @return int with the int the object represents
*/
private static int extractInt(Object objToGetInt) {
if (objToGetInt instanceof Integer) {
return (Integer) objToGetInt;
}
return Integer.parseInt(objToGetInt.toString());
}
/**
* @return the description of the given attribute in the shell
*/
public String getDescription(String text) throws Exception {
if (waitingForInput) {
return "Unable to get description: waiting for input.";
}
return client.execute("getDescription", new Object[] { text }).toString();
}
/**
* Common code to handle all cases of setting nextResponse so that the
* attached debug target can be notified of effective state.
*
* @param nextResponse new next response
*/
private void setNextResponse(InterpreterResponse nextResponse) {
this.nextResponse = nextResponse;
}
/**
* Wait for an established connection.
* @param monitor
* @throws Exception if no suitable response is received before the timeout
* @throws UserCanceledException if user cancelled with monitor
*/
public void hello(IProgressMonitor monitor) throws Exception, UserCanceledException {
int maximumAttempts = InteractiveConsolePrefs.getMaximumAttempts();
monitor.beginTask("Establishing Connection To Console Process", maximumAttempts);
try {
if (firstCommWorked) {
return;
}
// We'll do a connection attempt, we can try to
// connect n times (until the 1st time the connection
// is accepted) -- that's mostly because the server may take
// a while to get started.
String result = null;
for (int commAttempts = 0; commAttempts < maximumAttempts; commAttempts++) {
if (monitor.isCanceled())
throw new UserCanceledException("Canceled before hello was successful");
try {
Object execute = client.execute("hello", new Object[] { "Hello jsconsole" });
if (execute instanceof Object[]) {
Object[] resulta = (Object[]) execute;
execute = resulta[0];
}
result = execute.toString();
} catch (XmlRpcException e) {
// We'll retry in a moment
}
if ("Hello eclipse".equals(result)) {
firstCommWorked = true;
break;
}
try {
Thread.sleep(250);
} catch (InterruptedException e) {
// Retry now
}
monitor.worked(1);
}
if (!firstCommWorked) {
throw new Exception("Failed to receive suitable Hello response from console. Last msg received: "
+ result);
}
} finally {
monitor.done();
}
}
/**
* Not required for normal js console
*/
public void linkWithDebugSelection(boolean isLinkedWithDebug) {
throw new RuntimeException("Not implemented");
}
}